16.6 其他
从运行时的角度,整个进程内的对象可分为两类:一种,自然是从arena区域分配的用户对象;另一种,则是运行时自身运行和管理所需的对象,比如管理arena内存片段的mspan,提供无锁分配的mcache等等。
管理对象的生命周期并不像用户对象那样复杂,且类型和长度都相对固定,所以算法策略显然不用那么复杂。还有,它们相对较长的生命周期也不适合占用arena区域,否则会导致更多碎片。为此,运行时专门设计了FixAlloc固定分配器来为管理对象分配内存。
固定分配器使用相同的算法框架,只有相应参数不同。
mfixalloc.go
type fixalloc struct{ size uintptr // 固定分配长度 first unsafe.Pointer // 关联函数 arg unsafe.Pointer // 关联函数调用参数 list *mlink // 复用链表 chunk *byte // 内存块指针 nchunk uint32 // 内存块长度 inuse uintptr // 内存块已用长度 }
当运行时在初始化heap时,一共构建了4种固定分配器。
mheap.go
func mHeap_Init(h*mheap,spans_size uintptr) { fixAlloc_Init(&h.spanalloc,unsafe.Sizeof(mspan{}),recordspan, …) fixAlloc_Init(&h.cachealloc,unsafe.Sizeof(mcache{}),nil,nil, …) fixAlloc_Init(&h.specialfinalizeralloc,unsafe.Sizeof(specialfinalizer{}),nil, …) fixAlloc_Init(&h.specialprofilealloc,unsafe.Sizeof(specialprofile{}),nil, …) }
mfixalloc.go
func fixAlloc_Init(ffixalloc,size uintptr,first…,arg unsafe.Pointer,statuint64) { f.size=size f.first= *(*unsafe.Pointer)(unsafe.Pointer(&first)) f.arg=arg f.list=nil f.chunk=nil f.nchunk=0 f.inuse=0 f.stat=stat }
分配算法优先从复用链表获取内存,只在获取失败,或剩余空间不足时才获取新内存块。
mfixalloc.go
func fixAlloc_Alloc(f*fixalloc)unsafe.Pointer{ // 尝试从可用链表提取 if f.list!=nil{ v:=unsafe.Pointer(f.list) f.list=f.list.next f.inuse+=f.size return v }
// 如果剩余内存块已不足分配,则获取新内存块(16KB) if uintptr(f.nchunk) <f.size{ f.chunk= (*uint8)(persistentalloc(_FixAllocChunk,0,f.stat)) f.nchunk= _FixAllocChunk }
// 获取新内存块时执行关联函数(通常用作初始化和拷贝数据) v:= (unsafe.Pointer)(f.chunk) if f.first!=nil{ fn:= *(*func(unsafe.Pointer,unsafe.Pointer))(unsafe.Pointer(&f.first)) fn(f.arg,v) }
// 更新属性 f.chunk= (*byte)(add(unsafe.Pointer(f.chunk),f.size)) f.nchunk-=uint32(f.size) f.inuse+=f.size
return v }
固定分配器持有的这个16 KB内存块分自persistent区域。该区域在很多地方为运行时提供后备内存,目的同样是为了减少并发锁,减少内存申请系统调用。
malloc.go
type persistentAlloc struct{ base unsafe.Pointer off uintptr }
var globalAlloc struct{ persistentAlloc }
func persistentalloc(size,align uintptr,sysStat*uint64)unsafe.Pointer{ systemstack(func() { p=persistentalloc1(size,align,sysStat) }) return p }
func persistentalloc1(size,align uintptr,sysStat*uint64)unsafe.Pointer{ const( chunk =256<<10 maxBlock=64<<10 //VM reservation granularity is 64K on windows )
// 直接分配大于64KB的内存块 if size>=maxBlock{ return sysAlloc(size,sysStat) }
// 后备内存块存放位置(本地或全局) var persistent*persistentAlloc if mp!=nil&&mp.p!=0{ persistent= &mp.p.ptr().palloc }else{ persistent= &globalAlloc.persistentAlloc }
// 偏移位置对齐 persistent.off=round(persistent.off,align)
// 如果后备块空间不足,则重新申请 if persistent.off+size>chunk||persistent.base==nil{ // 申请新256KB后备内存 persistent.base=sysAlloc(chunk, &memstats.other_sys) persistent.off=0 }
// 截取所需内存块 p:=add(persistent.base,persistent.off) persistent.off+=size return p }
至于释放过程,只简单地放回复用链表即可。
mfixalloc.go
func fixAlloc_Free(f*fixalloc,p unsafe.Pointer) { f.inuse-=f.size v:= (*mlink)(p) v.next=f.list f.list=v }
recordspan
四个FixAlloc,只有mspan指定了关联函数recordspan,其作用是按需扩张h_allspans存储空间。h_allspans保存了所有span对象指针,供垃圾回收时遍历。
内存分配器spans区域虽然保存了page/span映射关系,但有很多重复,基于效率考虑,并不适合用来作为遍历对象。
mheap.go
var h_allspans[]*mspan
func mHeap_Init(h*mheap,spans_size uintptr) { fixAlloc_Init(&h.spanalloc,unsafe.Sizeof(mspan{}),recordspan, …) }
func recordspan(vh unsafe.Pointer,p unsafe.Pointer) { h:= (*mheap)(vh) s:= (*mspan)(p)
// 如果空间已满 … if len(h_allspans) >=cap(h_allspans) { // 计算新容量 n:=64*1024/ptrSize if n<cap(h_allspans)*3/2{ n=cap(h_allspans) *3/2 }
// 申请新内存空间(直接用指针写slice内部属性)
var new[]*mspan
sp:= (*slice)(unsafe.Pointer(&new))
sp.array=sysAlloc(uintptr(n)*ptrSize, &memstats.other_sys)
sp.len=len(h_allspans)
sp.cap=n
// 如果原空间有数据,则复制后释放
if len(h_allspans) >0{
// 拷贝数据
copy(new,h_allspans)
// 释放旧内存块
// 或由gcSweep->gcCopySpans释放
if h.allspans!=mheap_.gcspans{
sysFree(unsafe.Pointer(h.allspans), ...)
}
}
// 指向新空间
h_allspans=new
h.allspans= (**mspan)(unsafe.Pointer(sp.array))
}
// 注意: // 上面的扩张直接用mmap在arena以外申请空间 // 而append引发的扩张是在arena区域 // 基于管理目的的h_allspans不适合用于arena区域
h_allspans=append(h_allspans,s) h.nspan=uint32(len(h_allspans)) }